﻿using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;

using Utils.Weixin.Pay.Helper;
using Utils.Weixin.Pay.Http;
using Utils.Weixin.Pay.Notify;
using Utils.Weixin.Pay.Options;
using Utils.Weixin.Pay.Result;

namespace Utils.Weixin.Pay
{
    public class WeChatPay : IDisposable
    {
        protected readonly WeChatPayOptions options;

        protected readonly HttpClient httpClient;

        protected readonly ILogger<WeChatPay> logger;

        protected const string BASE_URL = "https://api.mch.weixin.qq.com/";

        protected readonly ICacheStrategy cacheStrategy;
 
        public WeChatPay(WeChatPayOptions options, ILogger<WeChatPay> logger, ICacheStrategy cacheStrategy)
        {
            if (options.PrivateKey.StartsWith("-----BEGIN PRIVATE KEY-----"))
            {
                options.PrivateKey = RSAFromPkcs8.ExtractFromPemFormat(options.PrivateKey);
            }
            this.options = options;
            this.httpClient = new HttpClient(new HttpApiV3Handler(options))
            {
                BaseAddress = new Uri(BASE_URL)
            };
            this.logger = logger;
            this.cacheStrategy = cacheStrategy.Change(6);
        }


        /// <summary>
        /// 创建支付
        /// </summary>
        /// <param name="appId"></param>
        /// <param name="out_trade_no"></param>
        /// <param name="amount_total"></param>
        /// <param name="description"></param>
        /// <param name="openid"></param>
        /// <param name="attach"></param>
        /// <param name="valid_minute"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentException"></exception>
        public virtual async Task<string> CreatePayJsApi(string appId, string out_trade_no, decimal amount_total, string description, string openid = null, string attach = null, int valid_minute = 15)
        {
            if (string.IsNullOrEmpty(out_trade_no))
                throw new ArgumentException($"“{nameof(out_trade_no)}”不能为 null 或空。", nameof(out_trade_no));

            if (string.IsNullOrEmpty(description))
                throw new ArgumentException($"“{nameof(description)}”不能为 null 或空。", nameof(description));

            if (string.IsNullOrEmpty(options.NotifyUrl))
                throw new ArgumentException($"“NotifyUrl”不能为 null 或空。", nameof(options.NotifyUrl));
            if (openid.IsNullOrEmpty())
                throw Error.SystemError("缺少参数：openid");

            var json = await httpClient.PostAsync("v3/pay/transactions/jsapi", new JsonContent(new
            {
                appid = appId,
                mchid = options.MerchantNo,
                description = description.SubstringByte(0, 120),
                out_trade_no,
                amount = new
                {
                    total = (int)(amount_total * 100),
                    currency = "CNY"
                },
                payer = openid.IsNullOrEmpty() ? null : new
                {
                    openid
                },
                attach,
                notify_url = options.NotifyUrl,
                time_expire = DateTime.Now.AddMinutes(valid_minute).ToString("yyyy-MM-ddTHH:mm:sszzz")
            })).ReadAsWxPayAsync<JObject>();

            return json["prepay_id"].ToString();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="out_trade_no"></param>
        /// <param name="out_refund_no"></param>
        /// <param name="reason"></param>
        /// <param name="refund"></param>
        /// <param name="total"></param>
        /// <exception cref="ThirdException">第三方错误</exception>
        /// <returns></returns>
        public virtual async Task<RefundResult> ApplyRefund(string out_trade_no, string out_refund_no, string reason, decimal refund, decimal total)
        {
            return await httpClient.PostAsync("v3/refund/domestic/refunds", new JsonContent(new
            {
                out_trade_no,
                out_refund_no,
                reason,
                notify_url = options.NotifyUrl,
                amount = new
                {
                    refund = (int)(refund * 100),
                    total = (int)(total * 100),
                    currency = "CNY"
                }
            }))
                .ReadAsWxPayAsync<RefundResult>();
        }

        /// <summary>
        /// 单次转账
        /// </summary>
        /// <exception cref="ThirdException">第三方错误</exception>
        /// <returns></returns>
        public Task<string> Transfer(string appId, string out_batch_no, string remark, decimal amount, string openid, string realName)
        {
            return TransferBatches(appId, out_batch_no, remark, new List<(decimal amount, string openid, string realName, string remark)>() { (amount, openid, realName, remark) });
        }

        /// <summary>
        /// 批量转账
        /// </summary>
        /// <exception cref="ThirdException">第三方错误</exception>
        /// <returns></returns>
        public async Task<string> TransferBatches(string appId, string out_batch_no, string remark, List<(decimal amount, string openid, string realName, string remark)> transferList)
        {
            using var encrypter = GetEncrypter();

            var total_amount = transferList.Sum(x => (int)x.amount * 100);
            var request = new HttpRequestMessage()
            {
                RequestUri = new Uri(BASE_URL + "v3/transfer/batches"),
                Method = HttpMethod.Post,
                Content = new JsonContent(new
                {
                    appid = appId,
                    out_batch_no,
                    batch_name = remark,
                    batch_remark = remark,
                    total_amount,
                    total_num = transferList.Count,
                    notify_url = options.NotifyUrl,
                    transfer_detail_list = transferList.Select(a => new
                    {
                        out_detail_no = StringHelper.BuilderNo(""),
                        transfer_amount = (int)(a.amount * 100),
                        transfer_remark = a.remark,
                        a.openid,
                        user_name = encrypter.Encrypt(a.realName)
                    }).ToArray()
                })
            };
            request.Headers.Add("Wechatpay-Serial", encrypter.SerialNo);
            var jtoken = await httpClient.SendAsync(request).ReadAsWxPayAsync<JObject>();
            return jtoken["batch_id"].ToString();
        }
        /// <summary>
        /// 支付包
        /// </summary>
        /// <param name="appId"></param>
        /// <param name="prepay_id"></param>
        /// <returns></returns>
        public Dictionary<string, string> GetPayPackage(string appId, string prepay_id)
        {
            var data = new Dictionary<string, string>
            {
                { "appId", appId },
                { "timeStamp", DateTime.Now.ToUnixTimestamp().ToString() },
                { "nonceStr", Guid.NewGuid().ToString("N") },
                { "package", "prepay_id=" + prepay_id }
            };
            string paySign = RSAFromPkcs8.RSASHA256Pkcs1Sign(string.Join('\n', data.Values) + '\n', options.PrivateKey);
            data.Add("signType", "RSA");
            data.Add("paySign", paySign);
            //  data.Remove("appId");
            return data;
        }


        /// <summary>
        /// 查询交易
        /// </summary>
        /// <param name="out_trade_no"></param>
        public virtual async Task<TransactionResult> QueryTransactions(string out_trade_no)
        {
            var obj = await httpClient.GetAsync($"v3/pay/transactions/out-trade-no/{out_trade_no}?mchid={options.MerchantNo}")
                    .ReadAsWxPayAsync<JObject>();
            return TransactionResult.Create(obj);
        }

        /// <summary>
        /// 退款查询
        /// </summary>
        /// <param name="out_refund_no"></param>
        /// <returns></returns>
        public virtual async Task<RefundResult> QueryRefund(string out_refund_no)
        {
            var obj = await httpClient.GetAsync($"v3/refund/domestic/refunds/{out_refund_no}")
                    .ReadAsWxPayAsync<JObject>();
            return RefundResult.Create(obj);
        }

        public async Task<NotifyResult> Notify(Stream requestBody)
        {
            using var reader = new StreamReader(requestBody, Encoding.UTF8);
            string str = await reader.ReadToEndAsync();
            var result = JsonConvert.DeserializeObject<NotifyResult>(str);
            if (!result.IsSuccess)
            {
                return result;
            }
            try
            {
                result.Decrypt(options.ApiV3);
                return result;
            }
            catch (Exception e)
            {
                logger.LogError(e, "回调异常");
                logger.LogInformation(str);
                throw Error.DefaultError("签名错误");
            }
        }

        private Encrypter GetEncrypter()
        {
            string key = "pay:cert:" + options.MerchantNo;
            lock (this)
            {
                var certResult = cacheStrategy.Get<CertificateResult>(key);
                if (certResult == null)
                {
                    var certs = GetCertificates().GetAwaiter().GetResult();
                    certResult = certs.OrderByDescending(a => a.ExpireTime).FirstOrDefault();
                    if (certResult == null)
                        throw Error.DefaultError("下载证书失败");
                    cacheStrategy.Add(key, certResult, (int)((certResult.ExpireTime - DateTime.Now).TotalSeconds - 3600));
                }
                return new Encrypter(certResult);
            }
        }

        /// <summary>
        /// 获取证书列表
        /// </summary>
        /// <returns></returns>
        public async Task<List<CertificateResult>> GetCertificates()
        {
            var result = await httpClient.GetAsync("v3/certificates").ReadAsWxPayAsync<JObject>();
            var arr = (JArray)result["data"];
            return arr.Select(a => Decrypt(a)).ToList();
        }


        private CertificateResult Decrypt(JToken token)
        {
            var cert = token.ToObject<CertificateResult>();
            var encryptCertificate = token["encrypt_certificate"];
            //var algorithm = encryptCertificate["algorithm"].ToString();
            var associatedData = encryptCertificate["associated_data"].ToString();
            var ciphertext = encryptCertificate["ciphertext"].ToString();
            var nonce = encryptCertificate["nonce"].ToString();
            string certificateText = Security.AESHelper.AesGcmDecrypt(options.ApiV3, associatedData, nonce, ciphertext);
            cert.CertificateText = certificateText;
            return cert;
        }

        private string RsaEncryptOAEP(CertificateResult cert, string message)
        {
            using var x509 = new X509Certificate2(Encoding.UTF8.GetBytes(cert.CertificateText));
            using var rsa = (RSA)x509.PublicKey.Key;
            var buff = rsa.Encrypt(Encoding.UTF8.GetBytes(message), RSAEncryptionPadding.OaepSHA1);
            return Convert.ToBase64String(buff);
        }

       

        public void Dispose()
        {
            cacheStrategy.CloseChange();
            httpClient.Dispose();
            GC.SuppressFinalize(this);
        }
    }
}
